﻿
using System;

namespace EmperialApps.WeatherSpark.Data {

    /// <summary>Contains helper methods for working with temperature values.</summary>
    public static class ConvertValue {

        /// <summary>Gets the precision with which temperature conversion is performed.</summary>
        public const sbyte TemperaturePrecision = 1;

        /// <summary>Gets the precision with which pressure conversion is performed.</summary>
        public const sbyte PressurePrecision = 1;

        /// <summary>Gets the precision with which distance conversion is performed.</summary>
        public const sbyte DistancePrecision = 1;

        /// <summary>Gets the precision with which speed conversion is performed.</summary>
        public const sbyte SpeedPrecision = 1;

        /// <summary>Gets the precision with which direction conversion is performed.</summary>
        public const sbyte DirectionPrecision = -1;

        /// <summary>Gets the precision with which percentage conversion is performed.</summary>
        public const sbyte PercentagePrecision = 2;

        /// <summary>Gets the number of degrees in a circle.</summary>
        public const double DegreesPerCircle = 360;

        /// <summary>Gets the number of radians in a circle.</summary>
        public const double RadiansPerCircle = 2 * Math.PI;

        /// <summary>Gets the number of hours in a day.</summary>
        public const int HoursPerDay = 24;

        private const double HectopascalsPerInchOfMercury = 33.86389;
        private const double KilometersPerMile = 1.60934;
        private const double MetersPerKilometer = 1000;
        private const double SecondsPerHour = 60 * 60;

        private const int PrimaryDirectionCount = 1 + (byte)CompassDirection.NNW;
        private static readonly double DirectionFactor = Math.Pow( 10, Math.Abs( DirectionPrecision ) );


        /// <summary>Performs no conversion on the specified value.</summary>
        public static readonly Func<double, double> Identity = delegate( double value ) {
            return value;
        };

        /// <summary>Converts a Fahrenheit temperature to Celsius.</summary>
        public static double ToCelsius( double fahrenheit ) {
            return Math.Round( ((fahrenheit - 32) * 5) / 9, TemperaturePrecision );
        }

        /// <summary>Converts a Celsius temperature to Fahrenheit.</summary>
        public static double ToFahrenheit( double celsius ) {
            return Math.Round( ((celsius * 9) / 5) + 32, TemperaturePrecision );
        }

        /// <summary>Converts a pressure in Inches of Mercury to Hectopascals.</summary>
        public static double ToHectopascals( double inchesOfMercury ) {
            return Math.Round( inchesOfMercury * HectopascalsPerInchOfMercury, PressurePrecision );
        }

        /// <summary>Converts a pressure in Hectopascals to Inches of Mercury.</summary>
        public static double ToInchesOfMercury( double hectopascals ) {
            return Math.Round( hectopascals / HectopascalsPerInchOfMercury, PressurePrecision );
        }

        /// <summary>Converts a distance in Miles to Kilometers.</summary>
        public static double ToKilometers( double miles ) {
            return Math.Round( miles * KilometersPerMile, DistancePrecision );
        }

        /// <summary>Converts a distance in Kilometers to Miles.</summary>
        public static double ToMiles( double kilometers ) {
            return Math.Round( kilometers / KilometersPerMile, DistancePrecision );
        }

        /// <summary>Converts a speed in Miles/Hour to Meters/Second.</summary>
        public static double ToMetersPerSecond( double milesPerHour ) {
            return Math.Round( milesPerHour * KilometersPerMile * MetersPerKilometer / SecondsPerHour, SpeedPrecision );
        }

        /// <summary>Converts a speed in Meters/Second to Miles/Hour.</summary>
        public static double ToMilesPerHour( double metersPerSecond ) {
            return Math.Round( metersPerSecond * SecondsPerHour / KilometersPerMile / MetersPerKilometer, SpeedPrecision );
        }

        /// <summary>Converts a direction in degrees to a <see cref="CompassDirection"/>.</summary>
        public static CompassDirection ToCompassDirection( double degrees ) {
            double direction = PrimaryDirectionCount * degrees / DegreesPerCircle;
            byte directionValue = (byte)(Math.Round( direction ) % PrimaryDirectionCount);
            return (CompassDirection)directionValue;
        }

        /// <summary>Converts a set of tile display values into a <see cref="ForecastTileMode"/> value.</summary>
        public static ForecastTileMode ToTileMode( Units units, ForecastDisplayMode displayMode, bool displayBack, bool displayWide ) {
            ForecastTileMode modeUnits = (ForecastTileMode)units;
            ForecastTileMode modeDisplay = displayMode == ForecastDisplayMode.Hour ? ForecastTileMode.Display_Hour : ForecastTileMode.Display_Day;
            ForecastTileMode backDisplay = displayBack ? ForecastTileMode.Display_Back : 0;
            ForecastTileMode wideDisplay = displayWide ? ForecastTileMode.Display_Wide : 0;
            return modeUnits | modeDisplay | backDisplay | wideDisplay;
        }

        /// <summary>Converts a <see cref="ForecastTileMode"/> value into a set of tile display values.</summary>
        public static void FromTileMode( ForecastTileMode mode, out Units units, out ForecastDisplayMode displayMode, out bool displayBack, out bool displayWide ) {
            units =
                (mode & ForecastTileMode.Unit_Imperial) != 0
                    ? Units.Imperial
                    : Units.Metric;

            displayMode =
                (mode & ForecastTileMode.Display_Hour) != 0
                    ? ForecastDisplayMode.Hour
                    : ForecastDisplayMode.Day;

            displayBack = (mode & ForecastTileMode.Display_Back) != 0;

            displayWide = (mode & ForecastTileMode.Display_Wide) != 0;
        }

        /// <summary>Converts a <see cref="YrnoSymbol"/> to a decimal value representing <see cref="ForecastData.PrecipitationPotential"/>.</summary>
        public static double ToPrecipitationPotential( YrnoSymbol symbol ) {
            switch( symbol ) {
                default:
                case YrnoSymbol.Fair:
                case YrnoSymbol.ClearSky:
                case YrnoSymbol.ClearSkyAndDark:
                    return 0.00;
                case YrnoSymbol.PartlyCloudy:
                case YrnoSymbol.PartlyCloudyAndDark:
                    return 0.10;
                case YrnoSymbol.Cloudy:
                    return 0.20;
                case YrnoSymbol.Fog:
                    return 0.30;
                case YrnoSymbol.RainShowers:
                case YrnoSymbol.SleetShowers:
                case YrnoSymbol.SnowShowers:
                case YrnoSymbol.RainShowersAndDark:
                case YrnoSymbol.SnowShowersAndDark:
                case YrnoSymbol.LightRainShowers:
                case YrnoSymbol.LightSleetShowers:
                case YrnoSymbol.LightSnowShowers:
                    return 0.50;
                case YrnoSymbol.LightRainShowersAndThunder:
                case YrnoSymbol.LightSleetShowersAndThunder:
                case YrnoSymbol.LightSnowShowersAndThunder:
                case YrnoSymbol.RainShowersAndThunder:
                case YrnoSymbol.SleetShowersAndThunder:
                case YrnoSymbol.SnowShowersAndThunder:
                    return 0.60;
                case YrnoSymbol.LightRain:
                case YrnoSymbol.LightSleet:
                case YrnoSymbol.LightSnow:
                case YrnoSymbol.Rain:
                case YrnoSymbol.Sleet:
                case YrnoSymbol.Snow:
                case YrnoSymbol.HeavyRainShowers:
                case YrnoSymbol.HeavySleetShowers:
                case YrnoSymbol.HeavySnowShowers:
                    return 0.70;
                case YrnoSymbol.LightRainAndThunder:
                case YrnoSymbol.LightSleetAndThunder:
                case YrnoSymbol.LightSnowAndThunder:
                case YrnoSymbol.RainAndThunder:
                case YrnoSymbol.SnowAndThunder:
                case YrnoSymbol.SleetAndThunder:
                case YrnoSymbol.HeavyRainShowersAndThunder:
                case YrnoSymbol.HeavySleetShowersAndThunder:
                case YrnoSymbol.HeavySnowShowersAndThunder:
                    return 0.80;
                case YrnoSymbol.HeavyRain:
                case YrnoSymbol.HeavySleet:
                case YrnoSymbol.HeavySnow:
                    return 0.90;
                case YrnoSymbol.HeavyRainAndThunder:
                case YrnoSymbol.HeavySleetAndThunder:
                case YrnoSymbol.HeavySnowAndThunder:
                    return 1.00;
            }
        }

        /// <summary>Converts a <see cref="YrnoSymbol"/> to a decimal value representing <see cref="ForecastData.SkyCover"/>.</summary>
        public static double ToSkyCover( YrnoSymbol symbol ) {
            switch( symbol ) {
                default:
                case YrnoSymbol.ClearSky:
                case YrnoSymbol.ClearSkyAndDark:
                    return 0.00;
                case YrnoSymbol.Fair:
                    return 0.20;
                case YrnoSymbol.Fog:
                    return 0.40;
                case YrnoSymbol.PartlyCloudy:
                case YrnoSymbol.PartlyCloudyAndDark:
                    return 0.50;
                case YrnoSymbol.Cloudy:
                    return 0.80;
                case YrnoSymbol.LightRainShowers:
                case YrnoSymbol.LightSleetShowers:
                case YrnoSymbol.LightSnowShowers:
                case YrnoSymbol.RainShowers:
                case YrnoSymbol.SleetShowers:
                case YrnoSymbol.SnowShowers:
                case YrnoSymbol.RainShowersAndDark:
                case YrnoSymbol.SnowShowersAndDark:
                case YrnoSymbol.HeavyRainShowers:
                case YrnoSymbol.HeavySleetShowers:
                case YrnoSymbol.HeavySnowShowers:
                case YrnoSymbol.LightRainShowersAndThunder:
                case YrnoSymbol.LightSleetShowersAndThunder:
                case YrnoSymbol.LightSnowShowersAndThunder:
                case YrnoSymbol.RainShowersAndThunder:
                case YrnoSymbol.SleetShowersAndThunder:
                case YrnoSymbol.SnowShowersAndThunder:
                case YrnoSymbol.HeavyRainShowersAndThunder:
                case YrnoSymbol.HeavySleetShowersAndThunder:
                case YrnoSymbol.HeavySnowShowersAndThunder:
                    return 0.90;
                case YrnoSymbol.LightRain:
                case YrnoSymbol.LightSleet:
                case YrnoSymbol.LightSnow:
                case YrnoSymbol.Rain:
                case YrnoSymbol.Sleet:
                case YrnoSymbol.Snow:
                case YrnoSymbol.LightRainAndThunder:
                case YrnoSymbol.LightSleetAndThunder:
                case YrnoSymbol.LightSnowAndThunder:
                case YrnoSymbol.SnowAndThunder:
                case YrnoSymbol.RainAndThunder:
                case YrnoSymbol.SleetAndThunder:
                case YrnoSymbol.HeavyRain:
                case YrnoSymbol.HeavySleet:
                case YrnoSymbol.HeavySnow:
                case YrnoSymbol.HeavyRainAndThunder:
                case YrnoSymbol.HeavySleetAndThunder:
                case YrnoSymbol.HeavySnowAndThunder:
                    return 1.00;
            }
        }

        /// <summary>Coerces a degree value to within the expected cricle range.</summary>
        public static double ToDirection( double value ) {
            double clipped = value > DegreesPerCircle || value < -DegreesPerCircle ? 0 : value;
            double oriented = (clipped + DegreesPerCircle) % DegreesPerCircle;
            double direction = Math.Round( oriented / DirectionFactor ) * DirectionFactor;
            return direction;
        }

        /// <summary>Converts an angle in degrees to radians.</summary>
        public static double ToRadians( double degrees ) {
            double radians = degrees * RadiansPerCircle / DegreesPerCircle;
            return radians;
        }

        /// <summary>Converts an angle in radians to degrees.</summary>
        public static double ToDegrees( double radians ) {
            double degrees = DegreesPerCircle * radians / RadiansPerCircle;
            return degrees;
        }

        /// <summary>Converts a percentage to a decimal value.</summary>
        public static double ToDecimal( double percentage ) {
            if( percentage < 0.0 )
                return double.NaN;

            return Math.Round( percentage / 100, PercentagePrecision );
        }

        /// <summary>Converts a decimal value to a percentage.</summary>
        public static double ToPercentage( double value ) {
            return Math.Round( value * 100, 0 );
        }

    }

}
